/*
 * object-inspect: licensed under the MIT license
 *
 * repository: https://github.com/substack/object-inspect
 *
 */
(function() {
  let hasMap = typeof Map === 'function' && Map.prototype;
  let mapSizeDescriptor = Object.getOwnPropertyDescriptor && hasMap ? Object.getOwnPropertyDescriptor(Map.prototype, 'size') : null;
  let mapSize = hasMap && mapSizeDescriptor && typeof mapSizeDescriptor.get === 'function' ? mapSizeDescriptor.get : null;
  let mapForEach = hasMap && Map.prototype.forEach;
  let hasSet = typeof Set === 'function' && Set.prototype;
  let setSizeDescriptor = Object.getOwnPropertyDescriptor && hasSet ? Object.getOwnPropertyDescriptor(Set.prototype, 'size') : null;
  let setSize = hasSet && setSizeDescriptor && typeof setSizeDescriptor.get === 'function' ? setSizeDescriptor.get : null;
  let setForEach = hasSet && Set.prototype.forEach;
  let booleanValueOf = Boolean.prototype.valueOf;
  let hasOwn = Object.prototype.hasOwnProperty || function (key) { return key in this; };

  return function inspect() {
    try {
      console.log(inspect_.apply(this, arguments));
    } catch(e) {
      console.error(e);
    }
  };

  function inspect_ (obj, opts, depth, seen) {
      if (!opts) opts = {};

      let maxDepth = opts.depth === undefined ? 5 : opts.depth;
      if (depth === undefined) depth = 0;
      if (depth >= maxDepth && maxDepth > 0 && obj && typeof obj === 'object') {
          return '[Object]';
      }

      if (seen === undefined) seen = [];
      else if (indexOf(seen, obj) >= 0) {
          return '[Circular]';
      }

      function inspect (value, from) {
          if (from) {
              seen = seen.slice();
              seen.push(from);
          }
          return inspect_(value, opts, depth + 1, seen);
      }

      if (typeof obj === 'string') {
          return inspectString(obj);
      }
      else if (typeof obj === 'function') {
          let name = nameOf(obj);
          return '[Function' + (name ? ': ' + name : '') + ']';
      }
      else if (obj === null) {
          return 'null';
      }
      else if (isSymbol(obj)) {
          let symString = Symbol.prototype.toString.call(obj);
          return typeof obj === 'object' ? 'Object(' + symString + ')' : symString;
      }
      else if (isElement(obj)) {
          let s = '<' + String(obj.nodeName).toLowerCase();
          let attrs = obj.attributes || [];
          for (let i = 0; i < attrs.length; i++) {
              s += ' ' + attrs[i].name + '="' + quote(attrs[i].value) + '"';
          }
          s += '>';
          if (obj.childNodes && obj.childNodes.length) s += '...';
          s += '</' + String(obj.nodeName).toLowerCase() + '>';
          return s;
      }
      else if (isArray(obj)) {
          if (obj.length === 0) return '[]';
          let xs = Array(obj.length);
          for (let i = 0; i < obj.length; i++) {
              xs[i] = has(obj, i) ? inspect(obj[i], obj) : '';
          }
          return '[ ' + xs.join(', ') + ' ]';
      }
      else if (isError(obj)) {
          let parts = [];
          for (let key in obj) {
              if (!has(obj, key)) continue;

              if (/[^\w$]/.test(key)) {
                  parts.push(inspect(key) + ': ' + inspect(obj[key]));
              }
              else {
                  parts.push(key + ': ' + inspect(obj[key]));
              }
          }
          if (parts.length === 0) return '[' + obj + ']';
          return '{ [' + obj + '] ' + parts.join(', ') + ' }';
      }
      else if (typeof obj === 'object' && typeof obj.inspect === 'function') {
          return obj.inspect();
      }
      else if (isMap(obj)) {
          let parts = [];
          mapForEach.call(obj, function (value, key) {
              parts.push(inspect(key, obj) + ' => ' + inspect(value, obj));
          });
          return 'Map (' + mapSize.call(obj) + ') {' + parts.join(', ') + '}';
      }
      else if (isSet(obj)) {
          let parts = [];
          setForEach.call(obj, function (value ) {
              parts.push(inspect(value, obj));
          });
          return 'Set (' + setSize.call(obj) + ') {' + parts.join(', ') + '}';
      }
      else if (typeof obj !== 'object') {
          return String(obj);
      }
      else if (isNumber(obj)) {
          return 'Object(' + Number(obj) + ')';
      }
      else if (isBoolean(obj)) {
          return 'Object(' + booleanValueOf.call(obj) + ')';
      }
      else if (isString(obj)) {
          return 'Object(' + inspect(String(obj)) + ')';
      }
      else if (!isDate(obj) && !isRegExp(obj)) {
          let xs = [], keys = [];
          for (let key in obj) {
            if (has(obj, key)) keys.push(key);
          }
          // keys = keys.sort();
          for (let i = 0; i < keys.length; i++) {
              let key = keys[i];
              if (/[^\w$]/.test(key)) {
                  xs.push(inspect(key) + ': ' + inspect(obj[key], obj));
              }
              else xs.push(key + ': ' + inspect(obj[key], obj));
          }
          if (xs.length === 0) return '{}';
          return '{ ' + xs.join(', ') + ' }';
      }
      else return String(obj);
  };

  function quote (s) {
      return String(s).replace(/"/g, '&quot;');
  }

  function isArray (obj) { return toStr(obj) === '[object Array]' }
  function isDate (obj) { return toStr(obj) === '[object Date]' }
  function isRegExp (obj) { return toStr(obj) === '[object RegExp]' }
  function isError (obj) { return toStr(obj) === '[object Error]' }
  function isSymbol (obj) { return toStr(obj) === '[object Symbol]' }
  function isString (obj) { return toStr(obj) === '[object String]' }
  function isNumber (obj) { return toStr(obj) === '[object Number]' }
  function isBoolean (obj) { return toStr(obj) === '[object Boolean]' }

  function has (obj, key) {
      return hasOwn.call(obj, key);
  }

  function toStr (obj) {
      return Object.prototype.toString.call(obj);
  }

  function nameOf (f) {
      if (f.name) return f.name;
      let m = f.toString().match(/^function\s*([\w$]+)/);
      if (m) return m[1];
  }

  function indexOf (xs, x) {
      if (xs.indexOf) return xs.indexOf(x);
      for (let i = 0, l = xs.length; i < l; i++) {
          if (xs[i] === x) return i;
      }
      return -1;
  }

  function isMap (x) {
      if (!mapSize) {
          return false;
      }
      try {
          mapSize.call(x);
          return true;
      } catch (e) {}
      return false;
  }

  function isSet (x) {
      if (!setSize) {
          return false;
      }
      try {
          setSize.call(x);
          return true;
      } catch (e) {}
      return false;
  }

  function isElement (x) {
      if (!x || typeof x !== 'object') return false;
      if (typeof HTMLElement !== 'undefined' && x instanceof HTMLElement) {
          return true;
      }
      return typeof x.nodeName === 'string'
          && typeof x.getAttribute === 'function'
      ;
  }

  function inspectString (str) {
      let s = str.replace(/(['\\])/g, '\\$1').replace(/[\x00-\x1f]/g, lowbyte);
      return "'" + s + "'";

      function lowbyte (c) {
          let n = c.charCodeAt(0);
          let x = { 8: 'b', 9: 't', 10: 'n', 12: 'f', 13: 'r' }[n];
          if (x) return '\\' + x;
          return '\\x' + (n < 0x10 ? '0' : '') + n.toString(16);
      }
  }
})();
